package it.unibo.refolding.pico.ui;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.IntPair;
import it.unibo.refolding.common.MyRuntimeException;
import it.unimi.dsi.fastutil.shorts.ShortList;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.ArrayUtils;

import weka.core.Attribute;
import weka.core.DenseInstance;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.neighboursearch.CoverTree;
import weka.core.neighboursearch.NearestNeighbourSearch;

import com.google.common.base.Preconditions;

public class PeakSlopesZDefListener implements MouseListener, MouseMotionListener {

  private static final Logger logger = Logger.getLogger(PeakSlopesZDefListener.class.getSimpleName());

  private final PeakSlopesZDefPanel panel;
  private final List<NearestNeighbourSearch> nns;
  private final PeakSlopesDrawer drawer;

  private ActionListener curveSelectionListener;

  public PeakSlopesZDefListener(PeakSlopesZDefPanel panel, PeakSlopesDrawer drawer) {
    this.panel = panel;
    this.nns = new ArrayList<>();
    for (int i = 0; i < panel.h.loopCount(); ++i) {
      nns.add(createNns(
        panel.h.sharedZ(), panel.h.sharedDef(), panel.h.retractFrom(i), panel.h.retractTo(i)));
    }
    this.drawer = drawer;
    this.curveSelectionListener = new CurveSelectionListener(panel, drawer);
  }

  private static class CurveSelectionListener implements ActionListener {

    private final PeakSlopesDrawer drawer;
    private final Component panel;

    public CurveSelectionListener(Component panel, PeakSlopesDrawer drawer) {
      this.panel = panel;
      this.drawer = drawer;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      int segment = Integer.parseInt(e.getActionCommand());
      if (segment != drawer.segment) {
        drawer.segment(segment);
        drawer.lowestPressedIndex = drawer.highestDraggedIndex = -1;
        panel.repaint();
      }
    }
  }

  private static NearestNeighbourSearch createNns(ShortList z, ShortList def, int from, int to) {
    NearestNeighbourSearch nns = new CoverTree();
    ArrayList<Attribute> attInfo = new ArrayList<>(2);
    attInfo.add(new Attribute("x"));
    attInfo.add(new Attribute("y"));
    Instances instances = new Instances("wth", attInfo, 0);
    for (int i = from; i < to; ++i) {
      instances.add(new DenseInstance(1, new double[] {z.getShort(i), def.getShort(i)}));
    }
    if (instances.numInstances() != 0) {
      try {
        nns.setInstances(instances);
      } catch (Exception e) {
        logger.log(Level.SEVERE, e.toString(), e);
        throw new MyRuntimeException(e.getMessage(), e);
      }
    }
    return nns;
  }

  @Override
  public void mouseClicked(MouseEvent e) {
    // needed?
    drawer.lowestPressedIndex = drawer.highestDraggedIndex = -1;
    int z = e.getX() + panel.minZ();
    double def = e.getY() / drawer.defScale + panel.minDef();
    if (e.isPopupTrigger() || e.isAltDown()) {
      int[] indices = nearestNeighbourIndices(z, def);
      Preconditions.checkState(indices.length != 0);
      for (int index : indices) {
        int indexRemoved = panel.h.removePeakSlopeCoveringIndex(drawer.segment, index);
        if (indexRemoved != -1) {
          panel.h.save();
          drawer.removedPeakSlope(indexRemoved);
          panel.repaint();
          break;
        }
      }
    }
  }

  public int[] nearestNeighbourIndices(int z, double def) {
    IntPair ip = nearestNeighbour(z, def);
    return panel.h.indicesOfZDef(drawer.segment, ip);
  }

  public IntPair nearestNeighbour(int z, double def) {
    Instance target = new DenseInstance(1, new double[] {z, def});
    Instance nearestNeighbour;
    try {
      nearestNeighbour = nns.get(drawer.segment).nearestNeighbour(target);
    } catch (Exception e1) {
      throw new RuntimeException(e1.getMessage(), e1);
    }
    double[] a = nearestNeighbour.toDoubleArray();
    assert a.length == 2;
    short x = (short) a[0];
    assert x == a[0];
    short y = (short) a[1];
    assert y == a[1];
    IntPair ip = IntPair.of(x, y);
    return ip;
  }

  @Override
  public void mouseEntered(MouseEvent e) {
  }

  @Override
  public void mouseExited(MouseEvent e) {
  }

  private int minOrMinusOne(int[] a) {
    if (a.length == 0) {
      return -1;
    }
    return CommonThings.min(a);
  }

  private int maxOrMinusOne(int[] a) {
    if (a.length == 0) {
      return -1;
    }
    return CommonThings.max(a);
  }

  @Override
  public void mousePressed(MouseEvent e) {
    logger.log(Level.FINE, "{0}", e);
    drawer.lowestPressedIndex = minOrMinusOne(nearestNeighbourIndices(
      e.getX() + panel.minZ(), e.getY() / drawer.defScale + panel.minDef()));
    drawer.highestDraggedIndex = -1;
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    logger.log(Level.FINE, "{0}", e);
    if (drawer.highestDraggedIndex != -1) {
      assert drawer.lowestPressedIndex != -1;
      int relativePeakIndex = maxOrMinusOne(nearestNeighbourIndices(
        e.getX() + panel.minZ(), e.getY() / drawer.defScale + panel.minDef()));
      if (relativePeakIndex >= drawer.highestDraggedIndex) {
        assert relativePeakIndex != -1;
        int[] o = panel.h.overlapping(drawer.segment, drawer.lowestPressedIndex, relativePeakIndex);
        IntPair[] ips = new IntPair[o.length];
        for (int i = 0; i < ips.length; ++i) {
          ips[i] = panel.h.get(drawer.segment, o[i]);
        }
        if (e.isPopupTrigger() || e.isAltDown() || e.isShiftDown() || e.isMetaDown()) {
          panel.h.removePeakSlopes(drawer.segment, o);
          try {
            panel.h.save();
          } catch (RuntimeException ex) {
            logger.warning(ex.toString());
            // Assume no peak slopes could be saved.
            for (IntPair ip : ips) {
              panel.h.addPeakSlope(drawer.segment, ip.x, ip.y);
            }
            o = ArrayUtils.EMPTY_INT_ARRAY;
          }
          drawer.removedPeakSlopes(o);
        } else {
          if (panel.h.defAtRelativeIndex(drawer.segment, relativePeakIndex) < panel.h.defAtRelativeIndex(
            drawer.segment, drawer.lowestPressedIndex)) {
            int insertionPoint = panel.h.addPeakSlope(
              drawer.segment, drawer.lowestPressedIndex, relativePeakIndex);
            if (insertionPoint != -1) {
              try {
                panel.h.save();
              } catch (RuntimeException ex) {
                logger.warning(ex.toString());
                panel.h.removePeakSlopes(drawer.segment, new int[] {insertionPoint});
                insertionPoint = -1;
              }
            }
            if (insertionPoint != -1) {
              drawer.addedPeakSlope(insertionPoint, drawer.lowestPressedIndex, relativePeakIndex);
            }
          } else {
            logger.log(Level.INFO, "Cannot add a non-ascending peak.");
          }
        }
      }
      drawer.lowestPressedIndex = drawer.highestDraggedIndex = -1;
      panel.repaint();
    }
  }

  @Override
  public void mouseDragged(MouseEvent e) {
    logger.log(Level.FINE, "{0}", e);
    if (drawer.lowestPressedIndex != -1) {
      int highestDraggedIndex = maxOrMinusOne(nearestNeighbourIndices(
        e.getX() + panel.minZ(), e.getY() / drawer.defScale + panel.minDef()));
      if (highestDraggedIndex >= drawer.lowestPressedIndex) {
        drawer.highestDraggedIndex = highestDraggedIndex;
        panel.repaint();
      }
    }
  }

  @Override
  public void mouseMoved(MouseEvent e) {
  }

  public ActionListener curveSelectionListener() {
    return curveSelectionListener;
  }
}
